﻿using System;
using System.Net;
using System.Net.Sockets;

namespace MessageHandler
{
	public class Connection
	{
		private static IMessageHandler messageHandler;

		private static Socket connectedSocket;
		private Configuration cfg;

		public Connection(IMessageHandler handler)
		{
			messageHandler = handler;
		}

		public void Connect(Configuration cfg)
		{
			if (cfg == null)
			{
				messageHandler.HandleError(new ChatException("Keine Serverdaten vorhanden"));
				return;
			}
			this.cfg = cfg;

			if (cfg.IpAddress == null)
			{
				messageHandler.HandleError(new ChatException("Keine IP-Adresse vorhanden"));
				return;
			}

			if (cfg.Port <= 0)
			{
				messageHandler.HandleError(new ChatException("Kein Port vorhanden"));
				return;
			}
			ConnectToServer();
		}

		private void ConnectToServer()
		{
			try
			{
				IPEndPoint ipEndPoint = GetIPEndPoint();
				connectedSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
				connectedSocket.Connect(ipEndPoint);

				ReceiveStateObject state = new ReceiveStateObject(this);
				state.connectedSocket = connectedSocket;
				connectedSocket.BeginReceive(state.buffer, 0, ReceiveStateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state);
			}
			catch (SocketException)
			{
				throw new ChatException("Verbindung konnte nicht hergestellt werden, da der Server nicht antwortet.",
					"Fehler im Verbindungsaufbau");
			}
		}

		private IPEndPoint GetIPEndPoint()
		{
			IPAddress ipAddress = GetIPAddress(cfg.IpAddress);
			IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, (int)cfg.Port);
			return ipEndPoint;
		}

		private IPAddress GetIPAddress(String hostIpOrName)
		{
			// Da mehrere Netzwerkkarten im Rechner sein können, muss auch
			// im Serverbetrieb die Netzwerkadresse berücksichtigt werden.

			// Wenn sich die Adresse parsen lässt (d.h. eins der üblichen Formate für IP-Adressen hat),
			// nehmen wir diese IP.
			// Sonst ist es wahrscheinlich ein Hostname, dann versuchen wir diesen aufzulösen
			// und verwenden die nächstbeste IP, die zu diesem Hostnamen existiert.
			IPAddress ipAddress = null;
			try
			{
				ipAddress = IPAddress.Parse(hostIpOrName);
			}
			catch (FormatException)
			{
				IPHostEntry ipHostEntry = Dns.GetHostEntry(hostIpOrName);
				ipAddress = ipHostEntry.AddressList[0];
			}
			return ipAddress;
		}

		/// <summary>
		/// Wird aufgerufen, wenn Datenpakete vom Server ankommen
		/// </summary>
		/// <param name="asyncResult"></param>
		public static void ReadCallback(IAsyncResult asyncResult)
		{
			ReceiveStateObject state = (ReceiveStateObject)asyncResult.AsyncState;

			int bytesRead = 0;
			try
			{
				bytesRead = state.connectedSocket.EndReceive(asyncResult);
			}
			catch (SocketException e)
			{
				if (e.ErrorCode == 10053) // Data-Transmission-Timeout/ Protocol Error
				{
					messageHandler.HandleError(new ChatException("Data-Transmission-Timeout: Nachricht konnte nicht empfangen werden"));
					state.connectedSocket.Close();
					return;
				}
				else if (e.ErrorCode == 995)
				{
					messageHandler.HandleError(new ChatException("Socket geschlossen"));
					return;
				}
				else
				{
					messageHandler.HandleError(new ChatException("Verbindung von Gegenstelle geschlossen"));
					state.connectedSocket.Close();

					return;
				}
			}
			catch (System.ObjectDisposedException)
			{
				messageHandler.HandleError(new ChatException("Verbindung geschlossen"));
				state.connectedSocket.Close();
				return;
			}

			if (bytesRead == 0)
			{
				messageHandler.HandleError(new ChatException("Verbindung von Gegenstelle geschlossen"));
				state.connectedSocket.Close();
				return;
			}

			if (state.messageLength == 0)
			{
				byte[] bytesAll = state.buffer;
				byte[] bytesLength = new byte[4];
				Array.Copy(bytesAll, bytesLength, 4);
				state.messageLength = BitConverter.ToInt32(bytesLength, 0);
				if (state.messageLength > 0 && state.messageLength >= bytesRead - 4 && state.messageLength < 4000000)
				{
					state.completeMessage = new byte[state.messageLength];
					Array.Copy(bytesAll, 4, state.completeMessage, state.currentPosition, bytesRead - 4);
					state.currentPosition = bytesRead - 4;
				}
				else
				{
					state = new ReceiveStateObject(state.connection) { connectedSocket = state.connectedSocket };
					state.buffer = new byte[ReceiveStateObject.BufferSize];
					if (connectedSocket != null && connectedSocket.Connected)
					{
						connectedSocket.BeginReceive(state.buffer, 0, ReceiveStateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state);
					}
					return;
				}
			}
			else
			{
				Array.Copy(state.buffer, 0, state.completeMessage, state.currentPosition, bytesRead);
				state.currentPosition += bytesRead;
			}

			if (state.currentPosition == state.completeMessage.Length)
			{
				messageHandler.HandleMessage(state.completeMessage);
				state = new ReceiveStateObject(state.connection) { connectedSocket = state.connectedSocket };
			}

			state.buffer = new byte[ReceiveStateObject.BufferSize];
			if (connectedSocket != null && connectedSocket.Connected)
			{
				connectedSocket.BeginReceive(state.buffer, 0, ReceiveStateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state);
			}
		}

		public void Send(byte[] dataBytes)
		{
			int streamLength = dataBytes.Length;
			byte[] lengthBytes = BitConverter.GetBytes(streamLength);
			byte[] byteAll = new byte[4 + streamLength];
			Array.Copy(lengthBytes, byteAll, 4);
			Array.Copy(dataBytes, 0, byteAll, 4, streamLength);

			try
			{
				if (connectedSocket != null)
				{
					int bytesSent = connectedSocket.Send(byteAll);
				}
			}
			catch
			{
				throw new ChatException("Nachricht konnte nicht gesendet werden");
			}
		}

		public void Close()
		{
			try
			{
				if (connectedSocket != null && connectedSocket.Connected)
				{
					connectedSocket.Shutdown(SocketShutdown.Both);
					connectedSocket.Close();
					connectedSocket = null;
				}
			}
			catch
			{
				throw new ChatException("Verbindung wurde bereits geschlossen");
			}
		}

		// State object for reading client data asynchronously
		public class ReceiveStateObject
		{
			// Connected Socket
			public Socket connectedSocket = null;
			// Receive buffer.
			public const int BufferSize = 1024;
			public byte[] buffer = new byte[BufferSize];

			// Connection
			public Connection connection;

			// Complete Message
			public byte[] completeMessage;
			public int currentPosition = 0;
			public int messageLength;

			public ReceiveStateObject(Connection connection)
			{
				this.connection = connection;
			}
		}
	}
}
